package lk.icta.core.service.impl;

import com.thoughtworks.xstream.XStream;
import lk.icta.common.*;
import lk.icta.common.entity.BaseEntity;
import lk.icta.common.entity.Contacts;
import lk.icta.common.entity.Document;
import lk.icta.common.entity.President;
import lk.icta.common.service.*;
import lk.icta.common.service.impl.*;
import lk.icta.core.dao.EventDAO;
import lk.icta.core.entity.*;
import lk.icta.core.service.*;
import lk.icta.user.entity.User;
import lk.icta.user.service.UserManager;
import lk.icta.user.service.impl.UserManagerImpl;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class ServiceAuditor implements MethodInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(ServiceAuditor.class);
    private final Map<Class, Class> serviceClasses;
    // private final List<Class> debugClasses;
    private final XStream xstream;
    private final boolean debugInputParameters;
    private final EventDAO eventDao;

    public ServiceAuditor(boolean captureDebugBeforeExecution, EventDAO eventDao) {
        this.debugInputParameters = captureDebugBeforeExecution;
        this.eventDao = eventDao;

        xstream = new XStream();
        xstream.omitField(User.class, "dsDivisions");
        xstream.omitField(User.class, "roles");

        xstream.alias("user", User.class);
        xstream.alias("land", Land.class);
        xstream.alias("landApplication", LandApplication.class);

        xstream.alias("alienation", Alienation.class);
        xstream.alias("landAssociation", LandAssociation.class);
        xstream.alias("appointmentSchedule", AppointmentSchedule.class);
        xstream.alias("kachchery", Kachchery.class);
        xstream.alias("mortgage", Mortgage.class);
        xstream.alias("objection", Objection.class);
        xstream.alias("schedule", Schedule.class);
        xstream.alias("president", President.class);
        xstream.alias("document", Document.class);
        xstream.alias("contacts", Contacts.class);
        xstream.alias("registeredPost", RegisteredPost.class);


        xstream.alias("localAuthority", LocalAuthority.class);
        xstream.alias("calenderSchedule", CalenderSchedule.class);
        xstream.alias("workflowItem", WorkflowItem.class);

        // the service classes that need to be audited
        serviceClasses = new HashMap<Class, Class>();

        serviceClasses.put(LandManagementService.class, LandManagementServiceImpl.class);
        serviceClasses.put(AlienationManagementService.class, AlienationManagementServiceImpl.class);
        serviceClasses.put(LandApplicationManagementService.class, LandApplicationManagementServiceImpl.class);
        serviceClasses.put(AppointmentScheduleManagementService.class, AppointmentScheduleManagementServiceImpl.class);
        serviceClasses.put(CalenderScheduleManagementService.class, CalenderScheduleManagementServiceImpl.class);
        serviceClasses.put(KachcheryManagementService.class, KachcheryManagementServiceImpl.class);
        serviceClasses.put(LandAssociationManagementService.class, LandAssociationManagementServiceImpl.class);
        serviceClasses.put(MortgageManagementService.class, MortgageManagementServiceImpl.class);
        serviceClasses.put(ObjectionManagementService.class, ObjectionManagementServiceImpl.class);
        serviceClasses.put(ScheduleManagementService.class, ScheduleManagementServiceImpl.class);
        serviceClasses.put(PresidentManagementService.class, PresidentManagementServiceImpl.class);
        serviceClasses.put(DocumentManagementService.class, DocumentManagementServiceImpl.class);
        serviceClasses.put(RegisteredPostManagementService.class, RegisteredPostManagementServiceImpl.class);
        serviceClasses.put(LocalAuthorityManagementService.class, LocalAuthorityManagementServiceImpl.class);
        serviceClasses.put(WorkflowService.class, WorkflowServiceImpl.class);
        serviceClasses.put(GISService.class, GISServiceImpl.class);
        serviceClasses.put(MailSenderService.class, MailSenderServiceImpl.class);
        serviceClasses.put(SmsSenderService.class, SmsSenderServiceImpl.class);
        serviceClasses.put(ContactsManager.class, ContactsManagerImpl.class);

        //the domain objects to be debug audited
      /*  debugClasses = new ArrayList<Class>();
        debugClasses.add(Land.class);
        debugClasses.add(Alienation.class);
        debugClasses.add(LandApplication.class);*/

    }

    /**
     * Intercepts the service level method call and captures debug information and saves error information
     * into the Event table within a new and separate transaction
     *
     * @param methodInvocation not applicable for documentation
     * @return not applicable for documentation
     * @throws Throwable not applicable for documentation
     */
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {

        logger.debug("===================Service Auditor invoked=================");

        // identify the actual method to be invoked via the Spring proxy
        final Method method = methodInvocation.getMethod();
        final Class serviceClass = serviceClasses.get(method.getDeclaringClass());

        // assume we don't need to audit this call
        boolean auditInvocation = false;

        // Do we need to audit this call? If this a transactional service call we should
        if (serviceClass != null) {
            Transactional transactional = serviceClass.getMethod(
                    method.getName(), method.getParameterTypes()).getAnnotation(Transactional.class);

            auditInvocation = (transactional != null && (
                    transactional.propagation().equals(Propagation.REQUIRED) ||
                            transactional.propagation().equals(Propagation.REQUIRES_NEW)));

            // if not a transactional method, capture those marked auditable
            if (!auditInvocation) {
                auditInvocation = serviceClass.getMethod(
                        method.getName(), method.getParameterTypes()).getAnnotation(Auditable.class) != null;
            }
        }

        // Do we need to debug this call? If this is audited, and debugInputParameters is set, then we should
        boolean debugInvocation = auditInvocation && debugInputParameters;

        if (logger.isDebugEnabled()) {
            logger.debug("Service method : " + method.getDeclaringClass().getName() + "." + method.getName() +
                    "() - audit : " + auditInvocation + " debug : " + debugInvocation);
        }

        // the Event for this invocation
        Event event = auditInvocation ? new Event() : null;

        // a reference to a possible exception to track the event ID associated with it
        IdentifiableException identifiableException = null;

        // execute the invoked method
        try {
            Object obj = methodInvocation.proceed();
            // if we are auditing this call, capture the User, if we are debugging, capture original input parameters
            if (auditInvocation) {
                processMethodParameters(methodInvocation, debugInvocation, event);
            }
            return obj;

        } catch (Exception e) {

            // if we didn't plan to audit this call, but encountered an error, still we need to log an event
            if (event == null) {
                event = new Event();
                // capture user and any debug information if possible
                processMethodParameters(methodInvocation, true, event);
            }
            event.setEventType(Event.Type.ERROR);

            // capture stack trace
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            event.setStackTrace(sw.toString());

            // if not one of our errors.. create an eLandRuntimeException, by reassigning e
            if (!(e instanceof ELandRuntimeException) && !(e instanceof ELandException)) {
                logger.error("Unexpected error encountered", e);
                e = new ELandRuntimeException("Unexpected error", ErrorCodes.UNKNOWN_ERROR, e);
            }

            // capture error code and re-throw
            identifiableException = (IdentifiableException) e;
            event.setEventCode(identifiableException.getErrorCode());
            throw e;

        } finally {

            // capture event log if an event is available
            if (event != null) {
                eventDao.addEvent(event);

                // if an exception was encountered, cross insert the event id into it
                if (identifiableException != null) {
                    identifiableException.setEventId(event.getIdUKey());
                    logger.debug("Captured event log for exception as ID : {}", event.getIdUKey());
                }
            }
        }
    }

    /**
     * Process method parameters and extract User and set debug information into the passed event object
     *
     * @param methodInvocation the method invocation
     * @param debugInvocation  true if method parameter debugging is required
     * @param event            the event to capture information into
     */
    private void processMethodParameters(MethodInvocation methodInvocation, boolean debugInvocation, Event event) {

        String className = methodInvocation.getMethod().getDeclaringClass().getSimpleName();

        event.setMethodName(methodInvocation.getMethod().getName());
        event.setClassName(className);

        // keep a StringBuilder to capture debug info if we encounter
        StringBuilder eventData = new StringBuilder();
        StringBuilder debugData = new StringBuilder(1024 * 10);

        if ("UserManager".equals(className)) {
            if (methodInvocation.getArguments() != null && methodInvocation.getArguments()[0] instanceof User) {
                debugData = setDebugData(debugData, methodInvocation.getArguments()[0]);
            }
        }

        // get hold of the user and debug info if captureDebugBeforeExecution is enabled
        for (Object arg : methodInvocation.getArguments()) {

            if (arg != null && arg instanceof User) {
                event.setUser((User) arg);

                // } else if (arg != null && debugInvocation && debugClasses.contains(arg.getClass())) {
            } else if (arg != null && debugInvocation && arg instanceof BaseEntity) {

                debugData = setDebugData(debugData, arg);
                event.setRecordId(((BaseEntity) arg).getId());

            } else if (arg != null && (arg instanceof Long || arg instanceof Integer)) {
                eventData.append(arg);
                eventData.append(",");

            } else if (arg != null && (arg instanceof String && ((String) arg).length() < 20)) {
                eventData.append(arg);
                eventData.append(",");
            }
            event.setEventData(eventData.toString());
        }

        // save any debug info if captured
        if (debugData != null) {
            event.setDebug(debugData.toString());
        }

        logger.debug("Captured user and or method parameters");
    }

    private StringBuilder setDebugData(StringBuilder debugData, Object arg) {
        // this is a class we are interested to debug
        try {
            debugData.append(xstream.toXML(arg));
            debugData.append("\n");
        } catch (Exception ignore) {
            debugData.append("Could not capture\n");
        }
        return debugData;
    }

}
